package cn.sylinx.horm.starter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import cn.sylinx.horm.config.OrmConfig;
import cn.sylinx.horm.config.OrmConfigHolder;
import cn.sylinx.horm.config.ServiceEnvironment;
import cn.sylinx.horm.config.specific.SpecificConfig;
import cn.sylinx.horm.config.specific.SpecificConfigHolder;
import cn.sylinx.horm.core.DynamicClient;
import cn.sylinx.horm.core.SqlClient;
import cn.sylinx.horm.core.datasource.NamedDataSource;
import cn.sylinx.horm.dialect.DbType;
import cn.sylinx.horm.exception.HORMException;
import cn.sylinx.horm.pool.DataSourceWrapperFactory;
import cn.sylinx.horm.resource.io.Resources;
import cn.sylinx.horm.util.ExceptionCatcher;
import cn.sylinx.horm.util.GLog;
import cn.sylinx.horm.util.RelaxedPropertyResolver;
import cn.sylinx.horm.util.StrKit;
import cn.sylinx.horm.util.Tuple;

/**
 * 通用java项目启动器
 * 
 * @author johnhan
 *
 */
public class CommonStarter {

    public final static String CONFIG_FILE = "h-orm.properties";
    private final static String MULTI_DATASOURCE_PREFIX = "horm.config.datasource.multids[";
    private final static String MULTI_DATASOURCE_KEY_TEMPLATE = "horm.config.datasource.multids[%d].";
    private final static String MULTI_DATASOURCE_SPECIFIC_CONFIG_TEMPLATE = "horm.config.datasource.multids[%d]."
            + SpecificConfig.DS_CONFIG_KEYWORD + ".";

    private String config = CONFIG_FILE;
    private Properties p;
    private final SqlClientInitializor sqlClientInitializor = new DefaultSqlClientInitializor();

    public CommonStarter() {
        this((String) null);
    }

    public CommonStarter(String config) {
        if (config != null && !config.isEmpty()) {
            this.config = config;
        }
        init();
    }

    public CommonStarter(Properties p) {
        this.p = p;
    }

    private void init() {
        try {
            p = Resources.getResourceAsProperties(config);
        } catch (IOException e) {
            // 配置文件未找到
            GLog.error("初始化异常", e);
            throw new HORMException("初始化异常", e);
        }
    }

    public void start() {
        // 解析配置
        Tuple t = parseProperties();
        OrmConfig ormConfig = t.getObject(0);
        // 初始化应用配置
        OrmConfigHolder.init(ServiceEnvironment.NORMAL, ormConfig);
        List<NamedDataSource> dataSourceList = t.getObject(1);
        // 初始化SqlClient
        String defaultDatasourceName = initSqlClient(dataSourceList, ormConfig);
        // 加载数据源个性配置
        loadDataSourceSpecificConfig(OrmConfigHolder.getOrmConfig(), defaultDatasourceName);
    }

    private void loadDataSourceSpecificConfig(OrmConfig ormConfig, String defaultDatasourceName) {

        Map<String, SpecificConfig> dsSpecificConfigMap = new HashMap<>();

        if (defaultDatasourceName == null) {

            SpecificConfig c = new SpecificConfig();
            c.setSqlStatOpen(ormConfig.isSqlStatOpen());
            c.setSqlClientInterceptorEnable(true);
            dsSpecificConfigMap.put(DynamicClient.DEFAULT_DS_NAME, c);

        } else {

            // 获取数据源个数
            int count = getMultiDataSourceCount();

            for (int i = 0; i < count; ++i) {
                String keyPrefix = String.format(MULTI_DATASOURCE_KEY_TEMPLATE, i);
                String specificConfigKey = String.format(MULTI_DATASOURCE_SPECIFIC_CONFIG_TEMPLATE, i);
                String name = getProperty(keyPrefix + "name");
                SpecificConfig dsSpecificConfig = loadOneDataSourceSpecificConfig(ormConfig, specificConfigKey);

                dsSpecificConfigMap.put(name, dsSpecificConfig);
                if (defaultDatasourceName.equals(name)) {
                    dsSpecificConfigMap.put(DynamicClient.DEFAULT_DS_NAME, dsSpecificConfig);
                }
            }
        }

        if (!dsSpecificConfigMap.isEmpty()) {
            SpecificConfigHolder.init(ormConfig, dsSpecificConfigMap);
        }
    }

    private SpecificConfig loadOneDataSourceSpecificConfig(OrmConfig ormConfig, String specificConfigKey) {

        SpecificConfig c = new SpecificConfig();
        c.setSqlStatOpen(ormConfig.isSqlStatOpen());
        c.setSqlClientInterceptorEnable(true);

        String sqlStatOpen = getProperty(specificConfigKey + "sql-stat-open", "true");
        c.setSqlStatOpen(ExceptionCatcher.call(() -> Boolean.valueOf(sqlStatOpen)));

        String sqlClientInterceptorEnable = getProperty(specificConfigKey + "sql-client-interceptor-enable", "true");
        c.setSqlClientInterceptorEnable(ExceptionCatcher.call(() -> Boolean.valueOf(sqlClientInterceptorEnable)));

        return c;
    }

    public static class NamedDataSourcePrimary extends NamedDataSource {

        private boolean primary = false;

        public boolean isPrimary() {
            return primary;
        }

        public void setPrimary(boolean primary) {
            this.primary = primary;
        }
    }

    /**
     * 初始化数据源，并且返回默认数据源
     * 
     * @param dataSourceList
     * @param ormConfig
     * @return
     */
    private String initSqlClient(List<NamedDataSource> dataSourceList, OrmConfig ormConfig) {

        if (dataSourceList == null || dataSourceList.isEmpty()) {
            throw new HORMException("数据源信息丢失");
        }
        if (!isMultiple()) {
            // 单数据源
            DynamicClient.register(initOneSqlClient(dataSourceList.get(0), ormConfig));
            return null;

        } else {
            List<NamedDataSourcePrimary> transferdNdsp = new ArrayList<>();
            int primaryMult = 0;
            NamedDataSourcePrimary primaryed = null;
            // 多数据源
            for (NamedDataSource namedDataSource : dataSourceList) {
                NamedDataSourcePrimary ndsp = (NamedDataSourcePrimary) namedDataSource;
                if (ndsp.isPrimary()) {
                    primaryed = ndsp;
                    primaryMult++;
                }
                if (primaryMult > 1) {
                    throw new HORMException("primary datasource should be only one!");
                }
                transferdNdsp.add(ndsp);
            }
            NamedDataSourcePrimary primaryJumpQueue = null;
            if (primaryed == null) {
                // 都没有设置primary datasource，则取第1个
                primaryed = transferdNdsp.get(0);
            }

            primaryJumpQueue = new NamedDataSourcePrimary();
            primaryJumpQueue.setDataSource(primaryed.getDataSource());
            primaryJumpQueue.setDataSourceName(DynamicClient.DEFAULT_DS_NAME);
            primaryJumpQueue.setDbType(primaryed.getDbType());
            primaryJumpQueue.setPrimary(true);

            GLog.info("default SqlClient --> [" + primaryed.getDataSourceName() + "]");

            transferdNdsp.add(primaryJumpQueue);
            // 初始化
            transferdNdsp.forEach(ndsp -> DynamicClient.register(initOneSqlClient(ndsp, ormConfig)));

            return primaryed.getDataSourceName();
        }
    }

    private SqlClient initOneSqlClient(NamedDataSource signleNamedDataSource, OrmConfig ormConfig) {
        return sqlClientInitializor.initSqlClient(signleNamedDataSource, ormConfig);
    }

    /**
     * 
     * 解析配置，获取应用配置 和 数据源信息
     * 
     * @return Tuple[0] : OrmConfig, Tuple[1] : List<NamedDataSource>
     */
    private Tuple parseProperties() {
        List<NamedDataSource> dataSourceList = new ArrayList<>();
        OrmConfig ormConfig = new OrmConfig();
        // 1）检查数据源类型
        boolean multiple = isMultiple();
        // horm.config.parse-sql-path-dbtype=false
        boolean isParseSqlPathDbtype = isParseSqlPathDbtype();
        ormConfig.setParseSqlPathDbtype(isParseSqlPathDbtype);
        // 2）检查数据源配置是否正确
        if (!multiple) {
            // 1.1）检查单数据源是否配置正确，并构造单数据源
            dataSourceList.add(checkAndGetSingleNamedDataSource());
        } else {
            // 1.2）检查多数据源是否配置正确，并且初始化构造多数据源
            dataSourceList.addAll(checkAndGetMultiNamedDataSource());
        }
        // 3）读取其它配置
        setAppConfigs(ormConfig);
        return Tuple.apply(ormConfig, dataSourceList);
    }

    private void setAppConfigs(OrmConfig ormConfig) {

        boolean cacheModelOnStart = ExceptionCatcher
                .call(() -> Boolean.valueOf(getProperty("horm.config.cache-model-on-start", "false")));
        ormConfig.setCacheModelOnStart(cacheModelOnStart);
        String commandScanPackage = getProperty("horm.config.command-scan-package");
        ormConfig.setCommandScanPackage(commandScanPackage);
        boolean debug = ExceptionCatcher.call(() -> Boolean.valueOf(getProperty("horm.config.debug", "false")));
        ormConfig.setDebug(debug);

        boolean cacheKeyMd5 = ExceptionCatcher
                .call(() -> Boolean.valueOf(getProperty("horm.config.cache-key-md5", "false")));
        ormConfig.setCacheKeyMd5(cacheKeyMd5);

        String modelMapStrategy = getProperty("horm.config.model-map-strategy");
        ormConfig.setModelMapStrategy(modelMapStrategy);
        String modelScanPackage = getProperty("horm.config.model-scan-package");
        ormConfig.setModelScanPackage(modelScanPackage);
        String mapperScanPackage = getProperty("horm.config.mapper-scan-package");
        ormConfig.setMapperScanPackage(mapperScanPackage);
        String mapperPostfix = getProperty("horm.config.mapper-postfix");
        if (!StrKit.isBlank(mapperPostfix)) {
            ormConfig.setMapperPostfix(mapperPostfix);
        }

        boolean caseSensitive = ExceptionCatcher
                .call(() -> Boolean.valueOf(getProperty("horm.config.case-sensitive", "false")));
        ormConfig.setCaseSensitive(caseSensitive);

        boolean printSqlUseTime = ExceptionCatcher
                .call(() -> Boolean.valueOf(getProperty("horm.config.print-sql-use-time", "false")));
        ormConfig.setPrintSqlUseTime(printSqlUseTime);


        boolean optimisticLockEnable = ExceptionCatcher
                .call(() -> Boolean.valueOf(getProperty("horm.config.optimistic-lock-enable", "false")));
        ormConfig.setOptimisticLockEnable(optimisticLockEnable);

        String sqlPostfix = getProperty("horm.config.sql-postfix");
        if (!StrKit.isBlank(sqlPostfix)) {
            ormConfig.setSqlPostfix(sqlPostfix);
        }

        int transactionIsolation = ExceptionCatcher
                .call(() -> Integer.valueOf(getProperty("horm.config.transaction-isolation", "2")));
        ormConfig.setTransactionIsolation(transactionIsolation);

        // sql 监控 --> horm.config.sql-stat-open
        boolean sqlStatOpen = ExceptionCatcher
                .call(() -> Boolean.valueOf(getProperty("horm.config.sql-stat-open", "false")));
        ormConfig.setSqlStatOpen(sqlStatOpen);

        // sql 监控 --> horm.config.sql-execute-time-threshold
        String sqlExecuteTimeThresholdStr = getProperty("horm.config.sql-execute-time-threshold",
                String.valueOf(OrmConfig.DEFAULT_STAT_THRESHOLD));
        long sqlExecuteTimeThreshold = ExceptionCatcher.call(() -> Long.valueOf(sqlExecuteTimeThresholdStr));
        ormConfig.setSqlExecuteTimeThreshold(sqlExecuteTimeThreshold);

        // 拦截器 --> SqlClient
        String interceptorClass = getProperty("horm.config.interceptor-class");
        if (!StrKit.isBlank(interceptorClass)) {
            ormConfig.setInterceptorClass(interceptorClass.trim());
        }

        // 拦截器 --> Mapper
        String mapperInterceptorClass = getProperty("horm.config.mapper-interceptor-class");
        if (!StrKit.isBlank(mapperInterceptorClass)) {
            ormConfig.setMapperInterceptorClass(mapperInterceptorClass.trim());
        }

        // 拦截器 --> Command
        String commandInterceptorClass = getProperty("horm.config.command-interceptor-class");
        if (!StrKit.isBlank(commandInterceptorClass)) {
            ormConfig.setCommandInterceptorClass(commandInterceptorClass.trim());
        }

    }

    private List<NamedDataSource> checkAndGetMultiNamedDataSource() {
        Set<String> nameUnique = new HashSet<>(8);
        List<NamedDataSource> dsList = new ArrayList<>();
        // 获取数据源个数
        int count = getMultiDataSourceCount();

        for (int i = 0; i < count; ++i) {
            String keyPrefix = String.format(MULTI_DATASOURCE_KEY_TEMPLATE, i);
            NamedDataSource nds = checkAndGetOneNamedDataSource(keyPrefix);
            if (nameUnique.contains(nds.getDataSourceName())) {
                throw new HORMException("datasource name should be unique");
            }
            nameUnique.add(nds.getDataSourceName());
            // horm.config.datasource.multids[1].primary
            // 多数据源下，决定哪个是默认使用
            String primaryKey = keyPrefix + "primary";
            boolean isPrimary = ExceptionCatcher.call(() -> Boolean.valueOf(getProperty(primaryKey, "false")));
            NamedDataSourcePrimary ndsp = toNamedDataSourcePrimary(nds, isPrimary);
            dsList.add(ndsp);
        }
        return dsList;
    }

    private NamedDataSourcePrimary toNamedDataSourcePrimary(NamedDataSource nds, boolean isPrimary) {
        NamedDataSourcePrimary ndsp = new NamedDataSourcePrimary();
        ndsp.setDataSource(nds.getDataSource());
        ndsp.setDataSourceName(nds.getDataSourceName());
        ndsp.setDbType(nds.getDbType());
        ndsp.setPrimary(isPrimary);
        return ndsp;
    }

    // horm.config.datasource.default-
    private NamedDataSource checkAndGetOneNamedDataSource(String prifix) {

        boolean isMultiple = isMultiple();
        String url = checkAndGetProperty(prifix + "url");
        String driver = checkAndGetProperty(prifix + "driver");
        String dbtype = checkAndGetProperty(prifix + "dbtype");
        DbType dbTypeEnum = DbType.getDbType(dbtype);
        GLog.debug("Use DbType:{}", dbTypeEnum.getValue());

        String pooltype = getProperty(prifix + "pooltype");
        String username = getProperty(prifix + "username");
        String password = getProperty(prifix + "password");
        String name = getProperty(prifix + "name");

        Map<String, Object> dsMap = new HashMap<>();
        dsMap.put("dbtype", dbtype);
        dsMap.put("pooltype", pooltype);
        dsMap.put("name", isMultiple ? name : DynamicClient.DEFAULT_DS_NAME);
        dsMap.put("url", url);
        dsMap.put("driver", driver);
        dsMap.put("username", username);
        dsMap.put("password", password);

        // 默认数据源额外配置
        String extConfigKey = prifix + "pool-config";
        Map<String, Object> extConfig = parseExtConfig(extConfigKey);
        // 获取单（默认）数据源

        return DataSourceWrapperFactory.buildDataSource(dsMap, extConfig);
    }

    private boolean isParseSqlPathDbtype() {
        return ExceptionCatcher
                .call(() -> Boolean.valueOf(p.getProperty("horm.config.parse-sql-path-dbtype", "false")));
    }

    private boolean isMultiple() {
        return ExceptionCatcher.call(() -> Boolean.valueOf(p.getProperty("horm.config.datasource.multiple", "false")));
    }

    private int getMultiDataSourceCount() {
        int max = -1;
        Set<Object> keySet = p.keySet();
        for (Object keyObj : keySet) {
            String key = keyObj.toString();
            if (key.startsWith(MULTI_DATASOURCE_PREFIX)) {
                int index = getMultiDataSourceIndex(key);
                if (index > max) {
                    max = index;
                }
            }
        }
        if (max < 0) {
            throw new HORMException("invalid multi datasource config");
        }
        return max + 1;
    }

    private static int getMultiDataSourceIndex(String key) {
        String keyReplaced = key.replace(MULTI_DATASOURCE_PREFIX, "");
        int index = keyReplaced.indexOf(']');
        if (index < 1) {
            throw new HORMException("invalid multi datasource config");
        }
        String digitStr = keyReplaced.substring(0, index).trim();
        return Integer.parseInt(digitStr);
    }

    private NamedDataSource checkAndGetSingleNamedDataSource() {
        return checkAndGetOneNamedDataSource("horm.config.datasource.default-");
    }

    /**
     * 获取对应额外配置
     * 
     * @param prefix
     * @return
     */
    private Map<String, Object> parseExtConfig(String prefix) {
        return new RelaxedPropertyResolver(p, prefix).toMap();
    }

    private String checkAndGetProperty(String key) {
        String v = getProperty(key);
        if (StrKit.isBlank(v)) {
            throw new HORMException("[" + key + "]未设置值");
        }
        return v;
    }

    private String getProperty(String key, String defaultValue) {
        String v = getProperty(key);
        if (v == null) {
            v = defaultValue;
        }
        return v;
    }

    private String getProperty(String key) {
        return p.getProperty(key);
    }

}
